/**
 * JSONRPC Encryption
 */
import RPC, {
  ClientOptions,
  RequestOptions,
  RequestError,
} from '@gdjiami/jsonrpc'

import { EncodeInfo, EncryptOptions } from './type'
import { AESDecrypt, AESEncrypt, getAESkey, getEncryptAESKey } from './utils'

const ENCODE_KEY = '__encode__'
const REFRESH_INTERVAL = 1000 * 60 * 30

class JSONRPC extends RPC {
  private static instance: JSONRPC
  public encodeInfo: EncodeInfo
  public encryptionOptions: EncryptOptions
  private timer = 0

  /**
   *
   * @param root 服务器地址
   * @param option ClientOptions 客户端设置
   * @param encryptType 加密模式，默认为 'RSA'
   * @param encryptionOptions 加密配置，必须配置 encryptType 才能生效
   */
  public static getInstance(
    root: string,
    option: ClientOptions = {},
    encryptType?: string,
    encryptionOptions?: EncryptOptions,
  ) {
    if (!this.instance) {
      return (this.instance = new JSONRPC(
        root,
        option,
        encryptType,
        encryptionOptions,
      ))
    }
    return this.instance
  }

  constructor(
    root: string,
    option: ClientOptions = {},
    encryptType?: string,
    encryptionOptions?: EncryptOptions,
  ) {
    super(root, option)
    if (encryptType) {
      this.encryptionOptions = encryptionOptions || {
        encryptType,
        encryptDigits: 16,
        pubKeyMethod: 'security.interaction.getPubKey',
        encodeInfoMethod: 'security.interaction.keyAgreement',
        refreshIdMethod: 'security.interaction.refresh',
      }

      if (process.env.NODE_ENV === 'development') {
        const {
          pubKeyMethod,
          encodeInfoMethod,
          refreshIdMethod,
        } = this.encryptionOptions

        if (!pubKeyMethod && !encodeInfoMethod && !refreshIdMethod) {
          throw new TypeError('请正确设置 encryptionOptions 配置')
        }
      }

      getAESkey(this.encryptionOptions.encryptDigits)
    }
  }

  /**
   * 初始化, 包括: 协商密钥, 定时刷新, 添加拦截器
   * @param update 重新协商操作无需再次添加拦截器
   */
  public async initial(update?: boolean) {
    if (process.env.NODE_ENV === 'development') {
      if (!JSONRPC.instance) {
        throw new TypeError('请生成 rpc 实例')
      }
    }

    await this.getEncodeInfo()

    this.setTimeoutToRefreshId()

    if (!update) {
      // 协商成功后添加拦截器
      this.addEncryptionInterceptor()
    }

    return () => {
      if (this.timer) {
        window.clearTimeout(this.timer)
        this.timer = 0
      }
    }
  }

  /**
   * 加密请求接口，返回的数据已经经过解密操作
   */
  public async encryptRequest<R, P = {}>(
    method: string,
    params?: P,
    options?: RequestOptions,
  ) {
    if (process.env.NODE_ENV === 'development') {
      if (!JSONRPC.instance) {
        throw new TypeError('请生成 rpc 实例')
      }
    }

    const encryptedData = AESEncrypt(window.btoa(JSON.stringify(params || {})))
    const res = await JSONRPC.instance.request<{ encryptedResult: string }>(
      method,
      { encryptedData },
      options,
    )
    /**
     * 暂时解决中文解码乱码问题
     * 有待观察
     */
    // return JSON.parse(window.atob(AESDecrypt(res.encryptedResult))) as R
    return JSON.parse(
      Buffer.from(AESDecrypt(res.encryptedResult), 'base64').toString(),
    ) as R
  }

  /**
   * 协商密钥
   */
  private getEncodeInfo = async () => {
    const {
      encryptType,
      pubKeyMethod,
      encodeInfoMethod,
      refreshInterval = REFRESH_INTERVAL,
    } = this.encryptionOptions

    try {
      const encodeInfo = sessionStorage.getItem(ENCODE_KEY)
      if (!encodeInfo) {
        const res = await JSONRPC.instance.request<{ pubKey: string }>(
          pubKeyMethod,
        )
        const result = await JSONRPC.instance.request<EncodeInfo>(
          encodeInfoMethod,
          { alg: encryptType, encryptedKey: getEncryptAESKey(res.pubKey) },
        )
        this.encodeInfo = {
          ...result,
          refreshTime: Date.now() + refreshInterval,
        }
        sessionStorage.setItem(ENCODE_KEY, JSON.stringify(this.encodeInfo))
      } else {
        this.encodeInfo = JSON.parse(encodeInfo) as EncodeInfo
      }
    } catch (error) {
      throw new Error(`协商密钥失败: ${error.message}`)
    }
  }

  /**
   * 设置刷新 securityId 延时器
   */
  private setTimeoutToRefreshId = () => {
    if (process.env.NODE_ENV === 'development') {
      if (!JSONRPC.instance) {
        throw new TypeError('请生成 rpc 实例')
      }
    }

    let timeout = this.encodeInfo.refreshTime - Date.now()

    if (timeout >= 0) {
      timeout = timeout > 1000 * 60 * 5 ? timeout - 1000 * 60 * 5 : timeout
      console.log(`将在 ${Math.floor(timeout / 1000)} s 刷新 securityId`)
      if (this.timer) {
        clearTimeout(this.timer)
        this.timer = 0
      }
      // 刷新间隔大于五分钟则提前五分钟进行刷新操作
      this.timer = window.setTimeout(() => {
        JSONRPC.instance.request(this.encryptionOptions.refreshIdMethod)
      }, timeout)
    } else {
      JSONRPC.instance.request(this.encryptionOptions.refreshIdMethod)
    }
  }

  /**
   * 添加拦截器, 加密请求时添加请求头
   */
  private addEncryptionInterceptor = () => {
    JSONRPC.instance.addInterceptor(async (request, xhr, next) => {
      // 是否是刷新请求
      const isRefreshRequest =
        request.method === this.encryptionOptions.refreshIdMethod

      if (request.params.encryptedData != undefined) {
        xhr.setRequestHeader('security-encrypted', 'true')
        xhr.setRequestHeader('security-id', this.encodeInfo.securityId)
      }
      if (isRefreshRequest) {
        xhr.setRequestHeader('security-id', this.encodeInfo.securityId)
      }

      try {
        return await next()
      } catch (error) {
        if (error) {
          if (isRefreshRequest || error.code === 642) {
            // 刷新操作失败 或者 securityId 失效, 需要重新协商
            try {
              sessionStorage.removeItem(ENCODE_KEY)
              console.warn('securityId 失效, 正在重新协商密钥')
              await this.initial(true)
              return await JSONRPC.instance.retry(request)
            } catch (err) {
              console.warn(err.message)
              throw err
            }
          }
        }

        throw error
      }
    })
  }
}

export { JSONRPC, ClientOptions, RequestOptions, RequestError }

export default (
  root: string,
  option: ClientOptions = {},
  encryptType = 'RSA',
  encryptionOptions?: EncryptOptions,
) => {
  return JSONRPC.getInstance(root, option, encryptType, encryptionOptions)
}
